package com.dhq.myapp.util

import android.text.TextUtils
import android.util.Log
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.io.*
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Executors
import javax.xml.transform.OutputKeys
import javax.xml.transform.Source
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource

object LogUtil {

    const val V = 0x01
    const val D = 0x02
    const val I = 0x04
    const val W = 0x08
    const val E = 0x10
    const val A = 0x20

    private const val FILE = 0xF1
    private const val JSON = 0xF2
    private const val XML = 0xF4

    private var dir: String? = null // 文件log存储目录

    var logSwitch = true // log总开关，默认开
        private set

    var log2FileSwitch = false // log写入文件开关，默认关
        private set

    var logFilter = V // log过滤器
        private set

    private const val TOP_BORDER = "═══════════════════════════════════════════════════════════════════════════════════════════════════"
    private const val BOTTOM_BORDER = "═══════════════════════════════════════════════════════════════════════════════════════════════════"
    private val LINE_SEPARATOR = System.getProperty("line.separator")
    private const val MAX_LEN = 4000
    private const val NULL_TIPS = "Log with null object."
    private const val NULL = "null"
    private const val ARGS = "args"

    private val executor = Executors.newSingleThreadExecutor()

    @JvmStatic
    fun v(contents: Any?) {
        log(V, null, contents)
    }

    @JvmStatic
    fun v(tag: String?, vararg contents: Any?) {
        log(V, tag, *contents)
    }

    @JvmStatic
    fun d(contents: Any?) {
        log(D, null, contents)
    }

    @JvmStatic
    fun d(tag: String?, vararg contents: Any?) {
        log(D, tag, *contents)
    }

    @JvmStatic
    fun i(contents: Any?) {
        log(I, null, contents)
    }

    @JvmStatic
    fun i(tag: String?, vararg contents: Any?) {
        log(I, tag, *contents)
    }

    @JvmStatic
    fun w(contents: Any?) {
        log(W, null, contents)
    }

    @JvmStatic
    fun w(tag: String?, vararg contents: Any?) {
        log(W, tag, *contents)
    }

    @JvmStatic
    fun e(contents: Any?) {
        log(E, null, contents)
    }

    @JvmStatic
    fun e(tag: String?, vararg contents: Any?) {
        log(E, tag, *contents)
    }

    @JvmStatic
    fun a(contents: Any?) {
        log(A, null, contents)
    }

    @JvmStatic
    fun a(tag: String?, vararg contents: Any?) {
        log(A, tag, *contents)
    }

    @JvmStatic
    fun file(contents: Any?) {
        log(FILE, null, contents)
    }

    @JvmStatic
    fun file(tag: String?, contents: Any?) {
        log(FILE, tag, contents)
    }

    @JvmStatic
    fun json(contents: String?) {
        log(JSON, null, contents)
    }

    @JvmStatic
    fun json(tag: String?, contents: String?) {
        log(JSON, tag, contents)
    }

    @JvmStatic
    fun xml(contents: String?) {
        log(XML, null, contents)
    }

    @JvmStatic
    fun xml(tag: String?, contents: String?) {
        log(XML, tag, contents)
    }

    private fun log(type: Int, tag: String?, vararg contents: Any?) {
        if (!logSwitch) return

        val processContents = processContents(type, tag, contents)
        val processedTag = processContents[0]
        val msg = processContents[1]

        when (type) {
            V, D, I, W, E, A -> {
                if (V == logFilter || type >= logFilter) {
                    printLog(type, processedTag, msg)
                }
                if (log2FileSwitch) {
                    print2File(processedTag, msg)
                }
            }
            FILE -> print2File(processedTag, msg)
            JSON, XML -> printLog(D, processedTag, msg)
        }
    }

    private fun processContents(type: Int, tag: String?, contents: Array<out Any?>): Array<String> {
        val targetElement = Thread.currentThread().stackTrace[5]
        var className = targetElement.className
        val classNameInfo = className.split(".").toTypedArray()
        if (classNameInfo.isNotEmpty()) {
            className = classNameInfo.last()
        }
        if (className.contains("$")) {
            className = className.split("\\.")[0]
        }

        val processedTag = if (TextUtils.isEmpty(tag)) className else tag!!

        val head = String.format(
            "Thread: %s, %s(%s.java:%d)$LINE_SEPARATOR",
            Thread.currentThread().name,
            targetElement.methodName,
            className,
            targetElement.lineNumber
        )

        var body = NULL_TIPS
        if (contents.isNotEmpty()) {
            body = if (contents.size == 1) {
                val obj = contents
                if (obj == null) NULL else obj.toString().let {
                    when (type) {
                        JSON -> formatJson(it)
                        XML -> formatXml(it)
                        else -> it
                    }
                }
            } else {
                val sb = StringBuilder()
                contents.forEachIndexed { i, content ->
                    sb.append(ARGS)
                        .append("[")
                        .append(i)
                        .append("]")
                        .append(" = ")
                        .append(content?.toString() ?: NULL)
                        .append(LINE_SEPARATOR)
                }
                sb.toString()
            }
        }

        var msg = head + body
        val lines = msg.split(LINE_SEPARATOR!!.toRegex()).toTypedArray()
        val sb = StringBuilder()
        lines.forEach { line ->
            sb.append(line).append(LINE_SEPARATOR)
        }
        msg = sb.toString()

        return arrayOf(processedTag, msg)
    }

    private fun formatJson(json: String): String {
        return try {
            when {
                json.startsWith("{") -> JSONObject(json).toString(4)
                json.startsWith("[") -> JSONArray(json).toString(4)
                else -> json
            }
        } catch (e: JSONException) {
            e.printStackTrace()
            json
        }
    }

    private fun formatXml(xml: String): String {
        return try {
            val xmlInput = StreamSource(StringReader(xml))
            val xmlOutput = StreamResult(StringWriter())
            val transformer = TransformerFactory.newInstance().newTransformer()
            transformer.setOutputProperty(OutputKeys.INDENT, "yes")
            transformer.setOutputProperty("{ http://xml.apache.org/xslt }indent-amount", "4")
            transformer.transform(xmlInput, xmlOutput)
            xmlOutput.writer.toString().replaceFirst(">".toRegex(), ">$LINE_SEPARATOR")
        } catch (e: Exception) {
            e.printStackTrace()
            xml
        }
    }

    private fun printLog(type: Int, tag: String, msg: String) {
        print(type, tag, TOP_BORDER)
        val len = msg.length
        val countOfSub = len / MAX_LEN
        if (countOfSub > 0) {
            print(type, tag, msg.substring(0, MAX_LEN))
            var index = MAX_LEN
            for (i in 1 until countOfSub) {
                print(type, tag, msg.substring(index, index + MAX_LEN))
                index += MAX_LEN
            }
            print(type, tag, msg.substring(index, len))
        } else {
            print(type, tag, msg)
        }
        print(type, tag, BOTTOM_BORDER)
    }

    private fun print(type: Int, tag: String, msg: String) {
        when (type) {
            V -> Log.v(tag, msg)
            D -> Log.d(tag, msg)
            I -> Log.i(tag, msg)
            W -> Log.w(tag, msg)
            E -> Log.e(tag, msg)
            A -> Log.wtf(tag, msg)
        }
    }

    @Synchronized
    private fun print2File(tag: String, msg: String) {
        val now = Date()
        val date = SimpleDateFormat("MM-dd", Locale.getDefault()).format(now)
        val fullPath = dir + date + ".txt"

        if (!createOrExistsFile(fullPath)) {
            Log.e(tag, "log to $fullPath failed!")
            return
        }

        val time = SimpleDateFormat("MM-dd HH:mm:ss.SSS ", Locale.getDefault()).format(now)
        val sb = StringBuilder()
        sb.append(TOP_BORDER).append(LINE_SEPARATOR)
            .append(time)
            .append(tag)
            .append(": ")
            .append(msg)
            .append(LINE_SEPARATOR)
            .append(LINE_SEPARATOR)
            .append(BOTTOM_BORDER).append(LINE_SEPARATOR)

        val dateLogContent = sb.toString()

        executor.execute {
            var bw: BufferedWriter? = null
            try {
                bw = BufferedWriter(FileWriter(fullPath, true))
                bw.write(dateLogContent)
                Log.d(tag, "log to $fullPath success!")
            } catch (e: IOException) {
                e.printStackTrace()
                Log.e(tag, "log to $fullPath failed!")
            } finally {
                try {
                    bw?.close()
                } catch (e: IOException) {
                    e.printStackTrace()
                }
            }
        }
    }

    private fun createOrExistsFile(filePath: String): Boolean {
        val file = File(filePath)
        if (file.exists()) return file.isFile
        if (!createOrExistsDir(file.parentFile)) return false
        return try {
            file.createNewFile()
        } catch (e: IOException) {
            e.printStackTrace()
            false
        }
    }

    private fun createOrExistsDir(file: File?): Boolean {
        return file != null && (if (file.exists()) file.isDirectory else file.mkdirs())
    }

    private fun isSpace(s: String?): Boolean {
        if (s == null) return true
        for (i in 0 until s.length) {
            if (!Character.isWhitespace(s[i])) {
                return false
            }
        }
        return true
    }

    @JvmStatic
    fun setEnable(isOpen: Boolean) {
        logSwitch = isOpen
    }
}
